/*
 * Copyright 2008 Neil Bacon
 * Distributed under the terms of the Mozilla Public License version 1.1
 * or the GNU General Public License version 3.
 */

package nb.xadd.operator.impl;

import gnu.trove.TIntDoubleProcedure;
import nb.cache.CacheMap;
import nb.xadd.NotYetImplemented;
import nb.xadd.node.Node;
import nb.xadd.node.NodeFactory;
import nb.xadd.node.impl.ConstantValueNode;
import nb.xadd.node.impl.LinearValueNode;
import nb.xadd.node.impl.PolynomialValueNode;
import nb.xadd.node.impl.PolynomialValueNode.CoefficientProcedure;
import nb.xadd.node.impl.PolynomialValueNode.PolynomialCoefficients;

public class Multiplication extends AbstractBinaryOperator {

    private final Node zero;
    
    protected Multiplication(NodeFactory nodeFactory, CacheMap<ApplyCacheKey, Node> applyCache) {
        super(nodeFactory, applyCache);
        zero = nodeFactory.getConstantValueNode(0.0);
    }

    @Override
    protected boolean isNoOp(ConstantValueNode c) {
        return c.getConstValue() == 1.0;
    }

    @Override
    public Node executeConstantConstant(ConstantValueNode n1, ConstantValueNode n2) {
        if (n1 == zero || n2 == zero) {
            return zero;
        }
        if (isNoOp(n1)) {
            return n2;
        }
        if (isNoOp(n2)) {
            return n1;
        }
        return getNodeFactory().getConstantValueNode(n1.getConstValue() * n2.getConstValue());
    }

    @Override
    public Node executeConstantLinear(ConstantValueNode n1, LinearValueNode n2) {
        if (n1 == zero) {
            return zero;
        }
        if (isNoOp(n1)) {
            return n2;
        }
        double[] coeff = n2.getCopyOfCoeff();
        for (int i = 0; i < coeff.length; i++) {
            coeff[i] *= n1.getConstValue();
        }
        return getNodeFactory().getLinearValueNode(n1.getConstValue() * n2.getConstValue(), coeff);
    }

    @Override
    public Node executeConstantPolynomial(ConstantValueNode n1, PolynomialValueNode n2) {
        if (n1 == zero) {
            return zero;
        }
        if (isNoOp(n1)) {
            return n2;
        }
        PolynomialCoefficients[] coeff = n2.getCopyOfCoeff();
        for (int i = 0; i < coeff.length; i++) {
            coeff[i].multiplyCoefficients(n1.getConstValue());
        }
        return getNodeFactory().getPolynomialValueNode(n2.getNumberOfVariables(), n1.getConstValue() * n2.getConstValue(), coeff);
    }

    @Override
    public Node executeLinearLinear(LinearValueNode n1, LinearValueNode n2) {
        throw new NotYetImplemented();
    }

    @Override
    public Node executeLinearPolynomial(LinearValueNode n1, PolynomialValueNode n2) {
        throw new NotYetImplemented();
    }

    @Override
    public Node executePolynomialPolynomial(final PolynomialValueNode n1, final PolynomialValueNode n2) {
        int numberOfVariables = Math.max(n1.getNumberOfVariables(), n2.getNumberOfVariables());
        final PolynomialCoefficients[] dest = new PolynomialCoefficients[n1.getPolynomialOrder() + n2.getPolynomialOrder()];
        for (int i = 0; i < dest.length; i++) {
            dest[i] = new PolynomialCoefficients(i + 1);
        }

        // We'll apply this to each term in n1.getCoeff()
        // which in turn will apply proc2 to each term in n2.getCoeff()
        // thus processing every pair.
        // It maybe more clumsy than nested loops, but it keeps us
        // independent of the implementation of PolynomialCoefficients.
        CoefficientProcedure proc1 = new CoefficientProcedure() {

            public void execute(final int[] index1, final double value1) {

                CoefficientProcedure proc2 = new CoefficientProcedure() {

                    public void execute(final int[] index2, final double value2) {
                        PolynomialCoefficients c = dest[index1.length + index2.length - 1];
                        c.accumulateProduct(value1 * value2, index1, index2);
                    }

                };
                for (PolynomialCoefficients c2 : n2.getCoeff()) {
                    c2.forEachEntry(proc2);
                }
            }

        };

        for (int i = 0; i < n1.getCoeff().length; i++) {
            final PolynomialCoefficients c1 = n1.getCoeff()[i];
            final int order = i + 1;
            c1.forEachEntry(proc1);

            addConstTermProducts(n2, dest, c1, order);
        }

        for (int i = 0; i < n2.getCoeff().length; i++) {
            final PolynomialCoefficients c2 = n2.getCoeff()[i];
            final int order = i + 1;

            addConstTermProducts(n1, dest, c2, order);
        }

        return getNodeFactory().getPolynomialValueNode(numberOfVariables, n1.getConstValue() * n2.getConstValue(), dest);
    }

    private void addConstTermProducts(
            final PolynomialValueNode n2,
            final PolynomialCoefficients[] dest,
            final PolynomialCoefficients c1, final int order) {
        // we'll apply this to each term c1 in n1.getCoeff()
        // to add the product of n2.getConstValue() and the term c1. 
        TIntDoubleProcedure const1 = new TIntDoubleProcedure() {

            public boolean execute(int key1, double value1) {
                PolynomialCoefficients c = dest[order - 1];
                c.accumulateProduct(n2.getConstValue() * value1, key1, order);
                return true;
            }

        };
        c1.forEachEntry(const1);
    }

}
